- Author: Dace Apšvalka with edits by Rik Henson
- Date: February 2025
- conda environment: mri conda environment.
fMRI Data Analysis: Group-Level Analysis¶
Once you have beta (or contrast) maps for conditions (or contrasts) from all subjects, you can perform group-level statistics. Importantly, all subject first-level results need to be in common space, e.g., MNI, to perform voxel-wise group analyses. Group-level analysis allows you to make inferences about the population, rather than individual subjects, by assessing common activations across participants. Common statistical methods for group-level analysis include one-sample or paired t-tests, as well as more complex ANOVA models, depending on your study design.
In this tutorial, we illustrate an ANOVA in which we include 9 contrasts, one for each of the 9 experimental conditions (averaged across runs), as well as 16 additional regressors to remove between-subject variance. This corresponds to a repeated-measures ANOVA, though assumes that the (pooled) error is spherical (see Rik's Stats tutorial section on ANOVA for the importance of this).
Here is a recommended viewing to help better understand the principles of the group-level analysis:
from IPython.display import YouTubeVideo
YouTubeVideo('__cOYPifDWk', width=560, height=315)
Table of contents
- Import required packages and set up some stuff
- Retrieve First-Level results
- Displaying subject Effects-Of-Interest z-maps
- Specify the second-level model
4.1. Design matrix for 3x3 repeated-measures ANOVA
4.2. Model specification and fit Contrasts
4.3. Contrasts - Computing contrasts and plotting result maps
5.1. Faces > Scrambled 5.2. Famous > Unfamiliar
5.3. Stimulus x Immediate Repetition interaction
5.4. FWE correction using non-parametric permutation testing - Summary results
6.1. Using atlasreader package
6.2. Nilearn's report
Import required packages and set up some stuff¶
# The conda environment used for this tutorial is available here: https://github.com/MRC-CBU/COGNESTIC/blob/main/mri_environment.yml
import os
import glob # to search for files using regex
import pandas as pd # for data manipulation
import numpy as np # for numerical operations
from bids.layout import BIDSLayout # to fetch data from BIDS-compliant datasets
import matplotlib.pyplot as plt # for basic plotting
import nibabel as nib # NiBabel, to read and write neuroimaging data, https://nipy.org/nibabel/
# Nilearn modules, for the analysis of brain volumes, plotting, etc., https://nilearn.github.io/
from nilearn.plotting import plot_glass_brain, plot_design_matrix, plot_contrast_matrix, plot_stat_map, view_img, view_img_on_surf
from nilearn.glm.second_level import SecondLevelModel
from nilearn.glm.thresholding import threshold_stats_img
from nilearn.datasets import load_mni152_template
from nilearn.glm.second_level import non_parametric_inference
from atlasreader import create_output # For generating result tables https://github.com/miykael/atlasreader
# MNI152 template will be used as a backgound for plotting results
mni152_template = load_mni152_template()
Retrieve First-Level results¶
For the group analysis, we will use the single-condition contrast estimate (beta estimate) maps for all nine conditions. Because we saved the results in BIDS format, we can us PyBIDS to retrieve the subject-level results.
# --- Set up the paths to the data and results folders
fmri_data_dir = 'example_data/FaceRecognition/data' # data in BIDS format
fmri_results_dir = 'example_data/FaceRecognition/results' # results in BIDS format
fmri_group_dir = os.path.join(fmri_results_dir, 'group-level') # where group results will go
os.makedirs(fmri_group_dir, exist_ok=True)
# --- Set up the BIDS layout
layout = BIDSLayout(fmri_data_dir, derivatives = True)
# Attach the results folder to the layout. It must complay with BIDS standards.
# And must include dataset_description.json file!
layout.add_derivatives(os.path.join(fmri_results_dir, "first-level"))
Displaying subject Effects-Of-Interest z-maps¶
To check how the first-level results look overall, it is helpful to display effects-of-interest for all subjects.
eoi_maps = layout.get(desc='EffectsOfInterest', extension='.nii.gz', return_type='file')
print(*eoi_maps, sep="\n")
/imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-02/sub-02_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-03/sub-03_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-04/sub-04_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-05/sub-05_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-06/sub-06_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-07/sub-07_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-08/sub-08_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-09/sub-09_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-10/sub-10_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-11/sub-11_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-12/sub-12_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-13/sub-13_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-14/sub-14_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-15/sub-15_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-16/sub-16_ses-mri_task-facerecognition_desc-EffectsOfInterest_z.nii.gz
subjects = sorted(layout.get_subjects())
fig, axes = plt.subplots(nrows=4, ncols=4, figsize=(14, 14))
for i, stat_map in enumerate(eoi_maps):
plot_glass_brain(stat_map,
title = 'sub-' + subjects[i],
axes = axes[int(i / 4), int(i % 4)],
plot_abs = False,
display_mode='x')
fig.suptitle('Effects of interest' + ', unthresholded z-maps')
Text(0.5, 0.98, 'Effects of interest, unthresholded z-maps')
Specify the second-level model¶
At the group-level analysis, we also use a GLM. The outcome variable is the beta/contrast estimate from each subject, and the predictor variables typically include group-level factors such as experimental conditions, subject-specific regressors (in repeated-measures designs), group-specific regressors (in between-subject designs), and subject-specific covariates (e.g., age, gender, or behavioural scores).
Design matrix for 3x3 repeated-measures ANOVA¶
In this example, the design matrix we generate will incorporate both within-subject (conditions) and between-subject (subjects) factors. Each row in the design matrix corresponds to a specific observation, which in this case is a beta estimate from a given condition and subject, while each column represents a predictor variable.
The number of rows in the design matrix must match the number of first-level result files that will be entered into the second-level model. The order of the rows in the design matrix must match the order of the provided files.
# Specify which conditions to include in the analysis and retrieve their effect files from the first-level results.
conditions = ['IniFF', 'ImmFF', 'DelFF', 'IniUF', 'ImmUF', 'DelUF', 'IniSF', 'ImmSF', 'DelSF']
effect_files = layout.get(desc=conditions, suffix='effect', extension='.nii.gz', return_type='filename')
# print to see if it found what we expexted
print(f"Found {len(effect_files)} effect files:")
print(*effect_files[0:10], sep="\n")
Found 144 effect files: /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-DelFF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-DelSF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-DelUF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-ImmFF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-ImmSF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-ImmUF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-IniFF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-IniSF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-01/sub-01_ses-mri_task-facerecognition_desc-IniUF_effect.nii.gz /imaging/correia/da05/workshops/2025-CBU/example_data/FaceRecognition/results/first-level/sub-02/sub-02_ses-mri_task-facerecognition_desc-DelFF_effect.nii.gz
Note we have reordered conditions so that the "stimulus" factor (Famous Face, Unfamiliar Face, Scrambled Face) rotates slowest, and "presentation" factor (Initial, Immediate Repeat, Delayed Repeat) rotates fastest in this 3x3 design.
# Create an empty DataFrame
df = pd.DataFrame(columns=conditions + subjects)
# Populate the DataFrame with 0s and 1s
for i, condition in enumerate(conditions):
# Filter files based on condition
condition_files = [1 if condition in file else 0 for file in effect_files]
# Add a column for the condition
df[condition] = condition_files
# Populate the DataFrame with 0s and 1s for subjects
for i, subject in enumerate(subjects):
# Filter files based on subject
subject_files = [1 if f"sub-{subject}" in file else 0 for file in effect_files]
# Add a column for the subject
df[subject] = subject_files
# Display the design matrix
design_matrix = df
ax = plot_design_matrix(design_matrix)
ax.set_title("Second level design matrix", fontsize=12)
ax.set_ylabel("stat maps")
Text(42.722222222222214, 0.5, 'stat maps')
The first 9 columns capture the mean of each condition, while the remaining 16 columns capture the mean of each subject (which we do not care about, and removing this variance can improve statistics for contrasts over the first 9 columns by reducing the residual error).
Model specification and fit¶
# specify
second_level_model = SecondLevelModel()
# fit
second_level_model = second_level_model.fit(
effect_files,
design_matrix = design_matrix
)
Contrasts¶
We can specify some contrasts that might be of interest. The first two are T-contrasts (one row), the last is an F-contrast (more than one row):
n_columns = design_matrix.shape[1]
contrasts = {
'FacesScrambled': np.pad([1/6, 1/6, 1/6, 1/6, 1/6, 1/6, -1/3, -1/3, -1/3], (0, n_columns - 9), 'constant'),
'FamousUnfamiliar': np.pad([1/3, 1/3, 1/3, -1/3, -1/3, -1/3, 0, 0, 0], (0, n_columns - 9), 'constant'),
'Stimulus X Immediate Repetition': np.array([np.pad(np.kron([1/2, 1/2, -1], [1, -1, 0]), (0, n_columns - 9), 'constant'),
np.pad(np.kron([ 1, -1, 0], [1, -1, 0]), (0, n_columns - 9), 'constant')])
}
for contrast_id, contrast_val in contrasts.items():
plot_contrast_matrix(contrast_val, design_matrix=design_matrix)
plt.suptitle(contrast_id)
Note that there is no particular scientific reason for the specific F-contrast interaction above, other than to demonstrate how to construct such contrasts.
Computing contrasts and plotting result maps¶
We could then loop through every contrast like below, using the same threshold:
for contrast_id, contrast_val in contrasts.items():
print(f"\tcontrast id: {contrast_id}")
z_map = second_level_model.compute_contrast(contrast_val, output_type="z_score")
thresholded_map, threshold = threshold_stats_img(
z_map,
alpha = 0.001,
height_control = None,
two_sided = True)
plot_stat_map(
thresholded_map,
threshold = threshold,
bg_img = mni152_template,
display_mode="ortho",
black_bg = True,
cmap = 'hot',
title = contrast_id)
fig = plt.gcf()
fig.set_size_inches(10,3)
plt.show()
but let's look at each contrast separately.
Faces > Scrambled¶
# get the z-maps for the contrast
z_map = second_level_model.compute_contrast(
contrasts['FacesScrambled'],
output_type='z_score')
thresholded_map_bonf, threshold_bonf = threshold_stats_img(
z_map,
alpha=0.05,
height_control='bonferroni',
two_sided=False)
print('Bonferroni p<.05 threshold: %.3f' % threshold_bonf)
plot_stat_map(
thresholded_map_bonf,
bg_img = mni152_template,
threshold=threshold_bonf,
display_mode = 'ortho',
black_bg = True,
cmap = 'hot',
title = 'Faces > Scrambled (Bonf. p<.05)'
)
fig = plt.gcf()
fig.set_size_inches(10,3)
plt.show()
Bonferroni p<.05 threshold: 5.052
Compared to one-sample t-test, we have many more degrees of freedom in this 3x3 ANOVA model, so more power. However, this power comes at the price of assuming that the error is spherical, i.e., that there is equal variance across the 9 conditions, and equal covariance. If this is not true, the statistics are biased, and hence the one-sample t-test approach is safer; see Rik's Stats for more details.
# Interactive plotting
plot = view_img(
thresholded_map_bonf,
bg_img=mni152_template,
threshold=threshold_bonf,
colorbar=True,
title='Faces > Scrambled (Bonf. p<.05)'
)
plot.open_in_browser()
/imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/numpy/core/fromnumeric.py:771: UserWarning: Warning: 'partition' will ignore the 'mask' of the MaskedArray. a.partition(kth, axis=axis, kind=kind, order=order)
Famous > Unfamiliar¶
Next, what about the famous vs unfamiliar contrast. This time we will use a one-tailed FDR correction:
z_map = second_level_model.compute_contrast(contrasts['FamousUnfamiliar'], output_type="z_score")
thresholded_map, threshold = threshold_stats_img(
z_map,
alpha = 0.05,
height_control = 'fdr',
two_sided = False)
plot_stat_map(
thresholded_map,
threshold = threshold,
bg_img = mni152_template,
display_mode="ortho",
black_bg = True,
cmap = 'hot',
title = 'Famous > Unfamiliar',
)
fig = plt.gcf()
fig.set_size_inches(10,3)
plt.show()
We can see some medial prefrontal regions that are more active for famous faces (that participants were likely to recognise, and may trigger semantic/emotional responses).
Stimulus x Immediate Repetition interaction¶
Finally, what about the interaction F-contrast we created, which asks whether the effect of repetition depends on whether the repeated stimulus is a face or scrambled face:
z_map = second_level_model.compute_contrast(contrasts['Stimulus X Immediate Repetition'], output_type="z_score") # Don't need stat_type="F" option?
thresholded_map, threshold = threshold_stats_img(
z_map,
alpha = 0.001,
height_control = None,
two_sided = True)
plot_stat_map(
thresholded_map,
threshold = threshold,
bg_img = mni152_template,
display_mode="ortho",
black_bg = True,
cmap = 'hot',
title = 'Stimulus X Immediate Repetition',
)
fig = plt.gcf()
fig.set_size_inches(10,3)
plt.show()
There is nothing particularly convincing for this last contrast: unlikely that anything would survive correction for multiple comparisons, but some might be reliable when using more focused Region-of-Interest (ROI) analyses as in the next Notebook (where we will also be able to plot the 9 conditions in order to understand interactions like this).
FWE correction using non-parametric permutation testing¶
Nilearn's FWE correction using the Bonferroni approach (height_control='bonferroni') is applied to the number of voxels. However, this method is not well-suited for fMRI data because neuroimaging data typically exhibit spatially correlated data points, which violate the Bonferroni assumption of independent tests.
As an alternative, neuroscientists have developed Random Field Theory (RFT), which accounts for spatial correlations by applying multiple comparison corrections that consider the smoothness of the data (it can be thought of as a Bonferroni correction based on the number of 'resels' - RESolution ELements - rather than the raw number of voxels). However, it makes various parametric assumptions about the data, including a minimal smoothness in terms of voxels, that can be violated. More practically, it is not implemented in Nilearn (to our knowledge).
Another alternative, which is implemented in Nilearn, is to use non-parametric inference, specifically permutation testing. This makes fewer assumptions when correcting for multiple comparisons (though note that is assumes exchangeability, which can also be violated if the error is nonspherical under the null; see Rik's Stats). The only downside is that permutation testing can take a long time to run.
out_dict = non_parametric_inference(
effect_files,
design_matrix = design_matrix,
second_level_contrast = contrasts['FacesScrambled'],
n_perm = 100, # ideally at least 10000
two_sided_test = False,
n_jobs = -1, # Use all available cores
threshold = 0.001 # cluster level threshold; enables cluster-level inference
)
# Print the keys of the output dictionary
print(out_dict.keys())
/imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/nilearn/mass_univariate/permuted_least_squares.py:986: UserWarning: Data array used to create a new image contains 64-bit ints. This is likely due to creating the array with numpy and passing `int` as the `dtype`. Many tools such as FSL and SPM cannot deal with int64 in Nifti images, so for compatibility the data has been converted to int32. image.new_img_like(masker.mask_img_, metric_map), /imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/nilearn/masking.py:980: UserWarning: Data array used to create a new image contains 64-bit ints. This is likely due to creating the array with numpy and passing `int` as the `dtype`. Many tools such as FSL and SPM cannot deal with int64 in Nifti images, so for compatibility the data has been converted to int32. return new_img_like(mask_img, unmasked, affine)
dict_keys(['t', 'logp_max_t', 'size', 'logp_max_size', 'mass', 'logp_max_mass'])
The output is multiple images (maps), organised in a dictionary.
- Voxel-level inference
- t: t-statistics
- logp_max_t: Negative log10 family-wise error rate-corrected p-values corrected based on the distribution of maximum t-statistics from permutations.
- Cluster-level inference
- size: Cluster size values associated with the significance test
- logp_max_size: Negative log10 family-wise error rate-corrected p-values corrected based on the distribution of maximum cluster sizes from permutations.
- mass: Cluster mass values associated with the significance test
- logp_max_mass: Negative log10 family-wise error rate-corrected p-values corrected based on the distribution of maximum cluster masses from permutations.
We will focus only on the voxel-level inference.
To report the FWE-corrected maps, we could display the logp_max_t; however, these values can be difficult to interpret if you're not familiar with them. It might be better to plot and report a t-map, masked to exclude the voxels that did not survive the FWE correction.
Let's create a new image displaying t-values for the voxels with a p-value < 0.05.
alpha = 0.05
masked = out_dict['logp_max_t'].get_fdata() > -np.log10(alpha)
masked_t_map = out_dict['t'].get_fdata() * masked
# save the masked t-map as a nifti image
masked_t_map_img = nib.Nifti1Image(masked_t_map, out_dict['t'].affine)
# Get the smallest t-value that is above the threshold (for the colorbar; the maps themselves are thresholded already)
threshold_fwe = masked_t_map[masked_t_map > 0].min()
print('FWE (perm.) p<.05 threshold: %.3f' % threshold_fwe)
plot_stat_map(
masked_t_map_img,
threshold = threshold_fwe,
display_mode = 'ortho',
black_bg = True,
bg_img = mni152_template,
cmap = 'hot',
title = f"Faces > Scrambled (FWE p <.{alpha})")
fig = plt.gcf()
fig.set_size_inches(10,3)
plt.show()
FWE (perm.) p<.05 threshold: 4.781
In this example, we observe that the non-parametric FWE correction is slightly less conservative than the Bonferroni correction.
Summary results¶
Using atlasreader package¶
We can use 'atlasreader' package to get summary results (peak table, cluster table, .png images of each cluster).
# generate and save atlasreader output
outdir = os.path.join(fmri_results_dir, 'group-level', 'permutation', 'FacesScrambled')
create_output(
masked_t_map_img,
cluster_extent = 20,
voxel_thresh = threshold_fwe,
direction = 'pos',
outdir = outdir
)
# display the peak table
peaks = glob.glob(os.path.join(outdir, '*_peaks.csv'))
display(pd.read_csv(peaks[0]))
| cluster_id | peak_x | peak_y | peak_z | peak_value | volume_mm | aal | desikan_killiany | harvard_oxford | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 36.0 | -88.0 | -12.0 | 10.967889 | 4472.0 | Occipital_Inf_R | ctx-rh-lateraloccipital | 48.0% Right_Lateral_Occipital_Cortex_inferior_... |
| 1 | 2.0 | -40.0 | -56.0 | -18.0 | 8.650674 | 4104.0 | Fusiform_L | ctx-lh-fusiform | 56.0% Left_Temporal_Occipital_Fusiform_Cortex;... |
| 2 | 3.0 | 42.0 | -54.0 | -18.0 | 9.290130 | 3440.0 | Fusiform_R | ctx-rh-fusiform | 65.0% Right_Temporal_Occipital_Fusiform_Cortex |
| 3 | 4.0 | 44.0 | -56.0 | 12.0 | 6.605022 | 1568.0 | Temporal_Mid_R | Right-Cerebral-White-Matter | 47.0% Right_Middle_Temporal_Gyrus_temporooccip... |
| 4 | 5.0 | 2.0 | 58.0 | -10.0 | 6.632364 | 1568.0 | Frontal_Med_Orb_L | Unknown | 69.0% Right_Frontal_Pole; 10.0% Right_Frontal_... |
| 5 | 6.0 | 22.0 | -8.0 | -14.0 | 7.339289 | 1112.0 | Hippocampus_R | Right-Amygdala | 97.0% Right_Amygdala |
| 6 | 7.0 | -20.0 | -10.0 | -14.0 | 7.604698 | 680.0 | Hippocampus_L | Left-Amygdala | 89.0% Left_Amygdala |
| 7 | 8.0 | -32.0 | -8.0 | -18.0 | 6.024115 | 384.0 | Hippocampus_L | Left-Cerebral-White-Matter | 15.0% Left_Amygdala; 10.0% Left_Hippocampus |
| 8 | 9.0 | -50.0 | -70.0 | 18.0 | 5.582745 | 248.0 | Temporal_Mid_L | ctx-lh-lateraloccipital | 69.0% Left_Lateral_Occipital_Cortex_superior_d... |
# display the cluster table
clusters = glob.glob(os.path.join(outdir, '*_clusters.csv'))
display(pd.read_csv(clusters[0]))
| cluster_id | peak_x | peak_y | peak_z | cluster_mean | volume_mm | aal | desikan_killiany | harvard_oxford | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 36.0 | -88.0 | -12.0 | 6.646250 | 4472.0 | 85.69% Occipital_Inf_R | 55.81% Right-Cerebral-White-Matter; 35.24% ctx... | 68.16% Right_Lateral_Occipital_Cortex_inferior... |
| 1 | 2.0 | -40.0 | -56.0 | -18.0 | 5.886170 | 4104.0 | 61.60% Fusiform_L; 21.64% Occipital_Inf_L | 28.46% Left-Cerebral-White-Matter; 27.29% ctx-... | 42.30% Left_Temporal_Occipital_Fusiform_Cortex... |
| 2 | 3.0 | 42.0 | -54.0 | -18.0 | 6.280087 | 3440.0 | 69.07% Fusiform_R; 16.98% Temporal_Inf_R; 5.81... | 40.00% Right-Cerebral-White-Matter; 37.67% ctx... | 82.09% Right_Temporal_Occipital_Fusiform_Corte... |
| 3 | 4.0 | 44.0 | -56.0 | 12.0 | 5.336986 | 1568.0 | 92.35% Temporal_Mid_R; 5.61% Occipital_Mid_R | 34.69% Right-Cerebral-White-Matter; 24.49% ctx... | 40.31% Right_Lateral_Occipital_Cortex_superior... |
| 4 | 5.0 | 2.0 | 58.0 | -10.0 | 5.502255 | 1568.0 | 53.06% Frontal_Med_Orb_L; 25.51% Frontal_Med_O... | 69.39% Unknown; 19.90% ctx-lh-medialorbitofron... | 40.31% Left_Frontal_Medial_Cortex; 30.61% Righ... |
| 5 | 6.0 | 22.0 | -8.0 | -14.0 | 5.473790 | 1112.0 | 60.43% Hippocampus_R; 31.65% Amygdala_R; 7.91%... | 62.59% Right-Amygdala; 19.42% Right-Hippocampu... | 82.73% Right_Amygdala; 17.27% Right_Hippocampus |
| 6 | 7.0 | -20.0 | -10.0 | -14.0 | 5.676894 | 680.0 | 63.53% Hippocampus_L; 21.18% Amygdala_L; 15.29... | 48.24% Left-Amygdala; 24.71% Left-Hippocampus;... | 81.18% Left_Amygdala; 18.82% Left_Hippocampus |
| 7 | 8.0 | -32.0 | -8.0 | -18.0 | 5.224707 | 384.0 | 58.33% Hippocampus_L; 22.92% Amygdala_L; 18.75... | 29.17% Left-Amygdala; 25.00% Left-Cerebral-Whi... | 68.75% Left_Amygdala; 31.25% Left_Hippocampus |
| 8 | 9.0 | -50.0 | -70.0 | 18.0 | 5.052146 | 248.0 | 93.55% Temporal_Mid_L; 6.45% Occipital_Mid_L | 54.84% ctx-lh-lateraloccipital; 32.26% Unknown... | 67.74% Left_Lateral_Occipital_Cortex_superior_... |
Some more plotting options
# get the top 5 peaks' x values
x = pd.read_csv(peaks[0])['peak_x'][:5]
# sort the x values
x = x.sort_values()
# plot these peaks
plot_stat_map(
masked_t_map_img,
threshold = threshold_fwe,
display_mode = 'x',
cut_coords = x,
black_bg = True,
bg_img = mni152_template,
cmap = 'hot',
title = f"Faces > Scrambled (FWE p <.{alpha})")
fig = plt.gcf()
fig.set_size_inches(15,3)
We can also look at a 3D brain using plotly.
view = view_img_on_surf(masked_t_map_img,
threshold = threshold_fwe)
#view.open_in_browser()
view.resize(1600, 800)
view
Or use, for example, FSLeyes to plot and explore the result maps.
Nilearn's report¶
Nilearn has a built-in report generator that can create reports for all defined contrasts. However, a limitation is that it cannot generate reports for results obtained using non-parametric inference.
second_level_report = second_level_model.generate_report(
contrasts,
title = "Results of the second-level analysis",
bg_img = mni152_template,
alpha = 0.001,
cluster_threshold = 20,
height_control = 'fpr',
min_distance = 8.0,
plot_type = 'slice',
display_mode = 'x',
report_dims = (1600, 800))
second_level_report.open_in_browser()
#second_level_report.save_as_html(file_name)
/imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/nilearn/plotting/displays/_slicers.py:308: UserWarning: empty mask ims = self._map_show(img, type="imshow", threshold=threshold, **kwargs) /imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/nilearn/reporting/get_clusters_table.py:302: UserWarning: The given float value must not exceed 0.0. But, you have given threshold=3.2905267314918945. stat_img = threshold_img( /imaging/correia/da05/conda/env/mri/lib/python3.11/site-packages/nilearn/reporting/glm_reporter.py:804: UserWarning: Attention: No clusters with stat higher than 3.2905267314918945 cluster_table = get_clusters_table(